package com.github.codeql

import com.github.codeql.utils.*
import com.github.codeql.utils.versions.*
import com.semmle.extractor.java.OdasaOutput
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.ir.*
import org.jetbrains.kotlin.backend.jvm.ir.*
import org.jetbrains.kotlin.codegen.JvmCodegenUtil
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
import org.jetbrains.kotlin.ir.declarations.*
import org.jetbrains.kotlin.ir.expressions.*
import org.jetbrains.kotlin.ir.symbols.*
import org.jetbrains.kotlin.ir.types.addAnnotations
import org.jetbrains.kotlin.ir.types.classFqName
import org.jetbrains.kotlin.ir.types.classifierOrNull
import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.types.isAny
import org.jetbrains.kotlin.ir.types.isNullableAny
import org.jetbrains.kotlin.ir.types.isPrimitiveType
import org.jetbrains.kotlin.ir.types.makeNullable
import org.jetbrains.kotlin.ir.types.typeOrNull
import org.jetbrains.kotlin.ir.types.typeWith
import org.jetbrains.kotlin.ir.types.typeWithArguments
import org.jetbrains.kotlin.ir.types.IrDynamicType
import org.jetbrains.kotlin.ir.types.IrErrorType
import org.jetbrains.kotlin.ir.types.IrSimpleType
import org.jetbrains.kotlin.ir.types.IrStarProjection
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.types.IrTypeArgument
import org.jetbrains.kotlin.ir.types.IrTypeProjection
import org.jetbrains.kotlin.ir.types.impl.*
import org.jetbrains.kotlin.ir.util.*
import org.jetbrains.kotlin.load.java.BuiltinMethodsWithSpecialGenericSignature
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.load.java.sources.JavaSourceElement
import org.jetbrains.kotlin.load.java.structure.*
import org.jetbrains.kotlin.load.java.typeEnhancement.hasEnhancedNullability
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.NameUtils
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.resolve.descriptorUtil.propertyIfAccessor
import org.jetbrains.kotlin.types.Variance
import org.jetbrains.kotlin.util.OperatorNameConventions

open class KotlinUsesExtractor(
    open val logger: Logger,
    open val tw: TrapWriter,
    val dependencyCollector: OdasaOutput.TrapFileManager?,
    val externalClassExtractor: ExternalDeclExtractor,
    val classInstanceStack: ClassInstanceStack,
    val primitiveTypeMapping: PrimitiveTypeMapping,
    val pluginContext: IrPluginContext,
    val globalExtensionState: KotlinExtractorGlobalState
) {
    fun referenceExternalClass(name: String) =
        getClassByFqName(pluginContext, FqName(name))?.owner.also {
            if (it == null) logger.warn("Unable to resolve external class $name")
            else extractExternalClassLater(it)
        }

    val javaLangObject by lazy { referenceExternalClass("java.lang.Object") }

    val javaLangObjectType by lazy { javaLangObject?.typeWith() }

    private fun usePackage(pkg: String): Label<out DbPackage> {
        return extractPackage(pkg)
    }

    fun extractPackage(pkg: String): Label<out DbPackage> {
        val pkgLabel = "@\"package;$pkg\""
        val id: Label<DbPackage> = tw.getLabelFor(pkgLabel, { tw.writePackages(it, pkg) })
        return id
    }

    fun useFileClassType(f: IrFile) =
        TypeResults(TypeResult(extractFileClass(f), "", ""), TypeResult(fakeKotlinType(), "", ""))

    fun useFileClassType(fqName: FqName) =
        TypeResults(
            TypeResult(extractFileClass(fqName), "", ""),
            TypeResult(fakeKotlinType(), "", "")
        )

    private fun useFileClassType(pkg: String, jvmName: String) =
        TypeResults(
            TypeResult(extractFileClass(pkg, jvmName), "", ""),
            TypeResult(fakeKotlinType(), "", "")
        )

    fun extractFileClass(f: IrFile): Label<out DbClassorinterface> {
        val pkg = f.packageFqName.asString()
        val jvmName = getFileClassName(f)
        val id = extractFileClass(pkg, jvmName)
        if (tw.lm.fileClassLocationsExtracted.add(f)) {
            val fileId = tw.mkFileId(f.path, false)
            val locId = tw.getWholeFileLocation(fileId)
            tw.writeHasLocation(id, locId)
        }
        return id
    }

    private fun extractFileClass(fqName: FqName): Label<out DbClassorinterface> {
        val pkg = if (fqName.codeQlIsRoot()) "" else fqName.parent().asString()
        val jvmName = fqName.shortName().asString()
        return extractFileClass(pkg, jvmName)
    }

    private fun extractFileClass(pkg: String, jvmName: String): Label<out DbClassorinterface> {
        val qualClassName = if (pkg.isEmpty()) jvmName else "$pkg.$jvmName"
        val label = "@\"class;$qualClassName\""
        val id: Label<DbClassorinterface> =
            tw.getLabelFor(label) {
                val pkgId = extractPackage(pkg)
                tw.writeClasses_or_interfaces(it, jvmName, pkgId, it)
                tw.writeFile_class(it)

                addModifiers(it, "public", "final")
            }
        return id
    }

    data class UseClassInstanceResult(
        val typeResult: TypeResult<DbClassorinterface>,
        val javaClass: IrClass
    )

    fun useType(t: IrType, context: TypeContext = TypeContext.OTHER): TypeResults {
        when (t) {
            is IrSimpleType -> return useSimpleType(t, context)
            else -> {
                logger.error("Unrecognised IrType: " + t.javaClass)
                return extractErrorType()
            }
        }
    }

    private fun extractJavaErrorType(): TypeResult<DbErrortype> {
        val typeId = tw.getLabelFor<DbErrortype>("@\"errorType\"") { tw.writeError_type(it) }
        return TypeResult(typeId, "<CodeQL error type>", "<CodeQL error type>")
    }

    private fun extractErrorType(): TypeResults {
        val javaResult = extractJavaErrorType()
        val kotlinTypeId =
            tw.getLabelFor<DbKt_nullable_type>("@\"errorKotlinType\"") {
                tw.writeKt_nullable_types(it, javaResult.id)
            }
        return TypeResults(
            javaResult,
            TypeResult(kotlinTypeId, "<CodeQL error type>", "<CodeQL error type>")
        )
    }

    fun getJavaEquivalentClass(c: IrClass) =
        getJavaEquivalentClassId(c)?.let { getClassByClassId(pluginContext, it) }?.owner

    /**
     * Gets a KotlinFileExtractor based on this one, except it attributes locations to the file that
     * declares the given class.
     */
    private fun withFileOfClass(cls: IrClass): KotlinFileExtractor {
        val clsFile = cls.fileOrNull

        if (this is KotlinFileExtractor && this.filePath == clsFile?.path) {
            return this
        }

        val newDeclarationStack =
            if (this is KotlinFileExtractor) this.declarationStack
            else KotlinFileExtractor.DeclarationStack()

        if (clsFile == null || isExternalDeclaration(cls)) {
            val filePath = getIrClassBinaryPath(cls)
            val newTrapWriter = tw.makeFileTrapWriter(filePath, true)
            val newLoggerTrapWriter = logger.dtw.makeFileTrapWriter(filePath, false)
            val newLogger = FileLogger(logger.loggerBase, newLoggerTrapWriter)
            return KotlinFileExtractor(
                newLogger,
                newTrapWriter,
                null,
                filePath,
                dependencyCollector,
                externalClassExtractor,
                classInstanceStack,
                primitiveTypeMapping,
                pluginContext,
                newDeclarationStack,
                globalExtensionState
            )
        }

        val newTrapWriter = tw.makeSourceFileTrapWriter(clsFile, true)
        val newLoggerTrapWriter = logger.dtw.makeSourceFileTrapWriter(clsFile, false)
        val newLogger = FileLogger(logger.loggerBase, newLoggerTrapWriter)
        return KotlinFileExtractor(
            newLogger,
            newTrapWriter,
            null,
            clsFile.path,
            dependencyCollector,
            externalClassExtractor,
            classInstanceStack,
            primitiveTypeMapping,
            pluginContext,
            newDeclarationStack,
            globalExtensionState
        )
    }

    // The Kotlin compiler internal representation of Outer<T>.Inner<S>.InnerInner<R> is
    // InnerInner<R, S, T>. This function returns just `R`.
    fun removeOuterClassTypeArgs(
        c: IrClass,
        argsIncludingOuterClasses: List<IrTypeArgument>?
    ): List<IrTypeArgument>? {
        return argsIncludingOuterClasses?.let {
            if (it.size > c.typeParameters.size) it.take(c.typeParameters.size) else null
        } ?: argsIncludingOuterClasses
    }

    private fun isStaticClass(c: IrClass) =
        c.visibility != DescriptorVisibilities.LOCAL && !c.isInner

    // Gets nested inner classes starting at `c` and proceeding outwards to the innermost enclosing
    // static class.
    // For example, for (java syntax) `class A { static class B { class C { class D { } } } }`,
    // `nonStaticParentsWithSelf(D)` = `[D, C, B]`.
    private fun parentsWithTypeParametersInScope(c: IrClass): List<IrDeclarationParent> {
        val parentsList = c.parentsWithSelf.toList()
        val firstOuterClassIdx = parentsList.indexOfFirst { it is IrClass && isStaticClass(it) }
        return if (firstOuterClassIdx == -1) parentsList
        else parentsList.subList(0, firstOuterClassIdx + 1)
    }

    // Gets the type parameter symbols that are in scope for class `c` in Kotlin order (i.e. for
    // `class NotInScope<T> { static class OutermostInScope<A, B> { class QueryClass<C, D> { } } }`,
    // `getTypeParametersInScope(QueryClass)` = `[C, D, A, B]`.
    private fun getTypeParametersInScope(c: IrClass) =
        parentsWithTypeParametersInScope(c).mapNotNull({ getTypeParameters(it) }).flatten()

    // Returns a map from `c`'s type variables in scope to type arguments
    // `argsIncludingOuterClasses`.
    // Hack for the time being: the substituted types are always nullable, to prevent downstream
    // code
    // from replacing a generic parameter by a primitive. As and when we extract Kotlin types we
    // will
    // need to track this information in more detail.
    private fun makeTypeGenericSubstitutionMap(
        c: IrClass,
        argsIncludingOuterClasses: List<IrTypeArgument>
    ) =
        getTypeParametersInScope(c)
            .map({ it.symbol })
            .zip(argsIncludingOuterClasses.map { it.withQuestionMark(true) })
            .toMap()

    fun makeGenericSubstitutionFunction(
        c: IrClass,
        argsIncludingOuterClasses: List<IrTypeArgument>
    ) =
        makeTypeGenericSubstitutionMap(c, argsIncludingOuterClasses).let {
            { x: IrType, useContext: TypeContext, pluginContext: IrPluginContext ->
                x.substituteTypeAndArguments(it, useContext, pluginContext)
            }
        }

    // The Kotlin compiler internal representation of Outer<A, B>.Inner<C, D>.InnerInner<E,
    // F>.someFunction<G, H>.LocalClass<I, J> is LocalClass<I, J, G, H, E, F, C, D, A, B>. This
    // function returns [A, B, C, D, E, F, G, H, I, J].
    private fun orderTypeArgsLeftToRight(
        c: IrClass,
        argsIncludingOuterClasses: List<IrTypeArgument>?
    ): List<IrTypeArgument>? {
        if (argsIncludingOuterClasses.isNullOrEmpty()) return argsIncludingOuterClasses
        val ret = ArrayList<IrTypeArgument>()
        // Iterate over nested inner classes starting at `c`'s surrounding top-level or static
        // nested class and ending at `c`, from the outermost inwards:
        val truncatedParents = parentsWithTypeParametersInScope(c)
        for (parent in truncatedParents.reversed()) {
            val parentTypeParameters = getTypeParameters(parent)
            val firstArgIdx =
                argsIncludingOuterClasses.size - (ret.size + parentTypeParameters.size)
            ret.addAll(
                argsIncludingOuterClasses.subList(
                    firstArgIdx,
                    firstArgIdx + parentTypeParameters.size
                )
            )
        }
        return ret
    }

    // `typeArgs` can be null to describe a raw generic type.
    // For non-generic types it will be zero-length list.
    fun useClassInstance(
        c: IrClass,
        typeArgs: List<IrTypeArgument>?,
        inReceiverContext: Boolean = false
    ): UseClassInstanceResult {
        val substituteClass = getJavaEquivalentClass(c)

        val extractClass = substituteClass ?: c

        // `KFunction1<T1,T2>` is substituted by `KFunction<T>`. The last type argument is the
        // return type.
        // Similarly Function23 and above get replaced by kotlin.jvm.functions.FunctionN with only
        // one type arg, the result type.
        // References to SomeGeneric<T1, T2, ...> where SomeGeneric is declared SomeGeneric<T1, T2,
        // ...> are extracted
        // as if they were references to the unbound type SomeGeneric.
        val extractedTypeArgs =
            when {
                extractClass.symbol.isKFunction() && typeArgs != null && typeArgs.isNotEmpty() ->
                    listOf(typeArgs.last())
                extractClass.fqNameWhenAvailable == FqName("kotlin.jvm.functions.FunctionN") &&
                    typeArgs != null &&
                    typeArgs.isNotEmpty() -> listOf(typeArgs.last())
                typeArgs != null && isUnspecialised(c, typeArgs, logger) -> listOf()
                else -> typeArgs
            }

        val classTypeResult = addClassLabel(extractClass, extractedTypeArgs, inReceiverContext)

        // Extract both the Kotlin and equivalent Java classes, so that we have database entries
        // for both even if all internal references to the Kotlin type are substituted.
        if (c != extractClass) {
            extractClassLaterIfExternal(c)
        }

        return UseClassInstanceResult(classTypeResult, extractClass)
    }

    private fun extractClassLaterIfExternal(c: IrClass) {
        if (isExternalDeclaration(c)) {
            extractExternalClassLater(c)
        }
    }

    private fun extractExternalEnclosingClassLater(d: IrDeclaration) {
        when (val parent = d.parent) {
            is IrClass -> extractExternalClassLater(parent)
            is IrFunction -> extractExternalEnclosingClassLater(parent)
            is IrFile -> logger.error("extractExternalEnclosingClassLater but no enclosing class.")
            is IrExternalPackageFragment -> {
                // The parent is a (multi)file class. We don't need
                // extract it separately.
            }
            else ->
                logger.error(
                    "Unrecognised extractExternalEnclosingClassLater ${parent.javaClass} for ${d.javaClass}"
                )
        }
    }

    private fun propertySignature(p: IrProperty) =
        ((p.getter ?: p.setter)?.extensionReceiverParameter?.let {
            useType(erase(it.type)).javaResult.signature
        } ?: "")

    fun getTrapFileSignature(d: IrDeclaration) =
        when (d) {
            is IrFunction ->
                // Note we erase the parameter types before calling useType even though the
                // signature should be the same
                // in order to prevent an infinite loop through useTypeParameter ->
                // useDeclarationParent -> useFunction
                // -> extractFunctionLaterIfExternalFileMember, which would result for `fun <T> f(t:
                // T) { ... }` for example.
                (listOfNotNull(d.extensionReceiverParameter) + d.valueParameters)
                    .map { useType(erase(it.type)).javaResult.signature }
                    .joinToString(separator = ",", prefix = "(", postfix = ")")
            is IrProperty -> propertySignature(d) + externalClassExtractor.propertySignature
            is IrField ->
                (d.correspondingPropertySymbol?.let { propertySignature(it.owner) } ?: "") +
                    externalClassExtractor.fieldSignature
            else ->
                "unknown signature"
                    .also { logger.warn("Trap file signature requested for unexpected element $d") }
        }

    private fun extractParentExternalClassLater(d: IrDeclaration) {
        val p = d.parent
        when (p) {
            is IrClass -> extractExternalClassLater(p)
            is IrExternalPackageFragment -> {
                // The parent is a (multi)file class. We don't need to
                // extract it separately.
            }
            else -> {
                logger.warn("Unexpected parent type ${p.javaClass} for external file class member")
            }
        }
    }

    private fun extractPropertyLaterIfExternalFileMember(p: IrProperty) {
        if (isExternalFileClassMember(p)) {
            extractParentExternalClassLater(p)
            val signature = getTrapFileSignature(p)
            dependencyCollector?.addDependency(p, signature)
            externalClassExtractor.extractLater(p, signature)
        }
    }

    private fun extractFieldLaterIfExternalFileMember(f: IrField) {
        if (isExternalFileClassMember(f)) {
            extractParentExternalClassLater(f)
            val signature = getTrapFileSignature(f)
            dependencyCollector?.addDependency(f, signature)
            externalClassExtractor.extractLater(f, signature)
        }
    }

    private fun extractFunctionLaterIfExternalFileMember(f: IrFunction) {
        if (isExternalFileClassMember(f)) {
            extractParentExternalClassLater(f)
            (f as? IrSimpleFunction)?.correspondingPropertySymbol?.let {
                extractPropertyLaterIfExternalFileMember(it.owner)
                // No need to extract the function specifically, as the property's
                // getters and setters are extracted alongside it
                return
            }
            val signature = getTrapFileSignature(f)
            dependencyCollector?.addDependency(f, signature)
            externalClassExtractor.extractLater(f, signature)
        }
    }

    fun extractExternalClassLater(c: IrClass) {
        dependencyCollector?.addDependency(c)
        externalClassExtractor.extractLater(c)
    }

    private fun tryReplaceAndroidSyntheticClass(c: IrClass): IrClass {
        // The Android Kotlin Extensions Gradle plugin introduces synthetic functions, fields and
        // classes. The most
        // obvious signature is that they lack any supertype information even though they are not
        // root classes.
        // If possible, replace them by a real version of the same class.
        if (
            c.superTypes.isNotEmpty() ||
                c.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB ||
                c.hasEqualFqName(FqName("java.lang.Object"))
        )
            return c
        return globalExtensionState.syntheticToRealClassMap.getOrPut(c) {
            val qualifiedName = c.fqNameWhenAvailable
            if (qualifiedName == null) {
                logger.warn(
                    "Failed to replace synthetic class ${c.name} because it has no fully qualified name"
                )
                return@getOrPut null
            }

            val result = getClassByFqName(pluginContext, qualifiedName)?.owner
            if (result != null) {
                logger.info("Replaced synthetic class ${c.name} with its real equivalent")
                return@getOrPut result
            }

            // The above doesn't work for (some) generated nested classes, such as R$id, which
            // should be R.id
            val fqn = qualifiedName.asString()
            if (fqn.indexOf('$') >= 0) {
                val nested = getClassByFqName(pluginContext, fqn.replace('$', '.'))?.owner
                if (nested != null) {
                    logger.info(
                        "Replaced synthetic nested class ${c.name} with its real equivalent"
                    )
                    return@getOrPut nested
                }
            }

            logger.warn("Failed to replace synthetic class ${c.name}")
            return@getOrPut null
        } ?: c
    }

    private fun tryReplaceFunctionInSyntheticClass(
        f: IrFunction,
        getClassReplacement: (IrClass) -> IrClass
    ): IrFunction {
        val parentClass = f.parent as? IrClass ?: return f
        val replacementClass = getClassReplacement(parentClass)
        if (replacementClass === parentClass) return f
        return globalExtensionState.syntheticToRealFunctionMap.getOrPut(f) {
            val result =
                replacementClass.declarations.findSubType<IrSimpleFunction> { replacementDecl ->
                    replacementDecl.name == f.name &&
                        replacementDecl.valueParameters.size == f.valueParameters.size &&
                        replacementDecl.valueParameters.zip(f.valueParameters).all {
                            erase(it.first.type) == erase(it.second.type)
                        }
                }
            if (result == null) {
                logger.warn("Failed to replace synthetic class function ${f.name}")
            } else {
                logger.info("Replaced synthetic class function ${f.name} with its real equivalent")
            }
            result
        } ?: f
    }

    fun tryReplaceSyntheticFunction(f: IrFunction): IrFunction {
        val androidReplacement =
            tryReplaceFunctionInSyntheticClass(f) { tryReplaceAndroidSyntheticClass(it) }
        return tryReplaceFunctionInSyntheticClass(androidReplacement) {
            tryReplaceParcelizeRawType(it)?.first ?: it
        }
    }

    fun tryReplaceAndroidSyntheticField(f: IrField): IrField {
        val parentClass = f.parent as? IrClass ?: return f
        val replacementClass = tryReplaceAndroidSyntheticClass(parentClass)
        if (replacementClass === parentClass) return f
        return globalExtensionState.syntheticToRealFieldMap.getOrPut(f) {
            val result =
                replacementClass.declarations.findSubType<IrField> { replacementDecl ->
                    replacementDecl.name == f.name
                }
                    ?: replacementClass.declarations
                        .findSubType<IrProperty> { it.backingField?.name == f.name }
                        ?.backingField
            if (result == null) {
                logger.warn("Failed to replace synthetic class field ${f.name}")
            } else {
                logger.info("Replaced synthetic class field ${f.name} with its real equivalent")
            }
            result
        } ?: f
    }

    private fun tryReplaceType(
        cBeforeReplacement: IrClass,
        argsIncludingOuterClassesBeforeReplacement: List<IrTypeArgument>?
    ): Pair<IrClass, List<IrTypeArgument>?> {
        val c = tryReplaceAndroidSyntheticClass(cBeforeReplacement)
        val p = tryReplaceParcelizeRawType(c)
        return Pair(p?.first ?: c, p?.second ?: argsIncludingOuterClassesBeforeReplacement)
    }

    private fun avoidInfiniteRecursion(
        pair: Pair<IrClass, List<IrTypeArgument>?>
    ): Pair<IrClass, List<IrTypeArgument>?> {
        val c = pair.first
        val args = pair.second
        if (args != null && args.isNotEmpty() && classInstanceStack.possiblyCyclicExtraction(c, args)) {
            logger.warn("Making use of ${c.name} a raw type to avoid infinite recursion")
            return Pair(c, null)
        } else {
            return pair
        }
    }

    // `typeArgs` can be null to describe a raw generic type.
    // For non-generic types it will be zero-length list.
    private fun addClassLabel(
        cBeforeReplacement: IrClass,
        argsIncludingOuterClassesBeforeReplacement: List<IrTypeArgument>?,
        inReceiverContext: Boolean = false
    ): TypeResult<DbClassorinterface> {
        val replaced =
            avoidInfiniteRecursion(tryReplaceType(cBeforeReplacement, argsIncludingOuterClassesBeforeReplacement))
        val replacedClass = replaced.first
        val replacedArgsIncludingOuterClasses = replaced.second

        val classLabelResult = getClassLabel(replacedClass, replacedArgsIncludingOuterClasses)

        var instanceSeenBefore = true

        val classLabel: Label<out DbClassorinterface> =
            tw.getLabelFor(classLabelResult.classLabel) {
                instanceSeenBefore = false

                extractClassLaterIfExternal(replacedClass)
            }

        if (
            replacedArgsIncludingOuterClasses == null ||
                replacedArgsIncludingOuterClasses.isNotEmpty()
        ) {
            // If this is a generic type instantiation or a raw type then it has no
            // source entity, so we need to extract it here
            val shouldExtractClassDetails =
                inReceiverContext &&
                    tw.lm.genericSpecialisationsExtracted.add(classLabelResult.classLabel)
            if (!instanceSeenBefore || shouldExtractClassDetails) {
                this.withFileOfClass(replacedClass)
                    .extractClassInstance(
                        classLabel,
                        replacedClass,
                        replacedArgsIncludingOuterClasses,
                        !instanceSeenBefore,
                        shouldExtractClassDetails
                    )
            }
        }

        val fqName = replacedClass.fqNameWhenAvailable
        val signature =
            if (replacedClass.isAnonymousObject) {
                null
            } else if (fqName == null) {
                logger.error("Unable to find signature/fqName for ${replacedClass.name}")
                null
            } else {
                fqName.asString()
            }
        return TypeResult(classLabel, signature, classLabelResult.shortName)
    }

    private fun tryReplaceParcelizeRawType(c: IrClass): Pair<IrClass, List<IrTypeArgument>?>? {
        if (
            c.superTypes.isNotEmpty() ||
                c.origin != IrDeclarationOrigin.DEFINED ||
                c.hasEqualFqName(FqName("java.lang.Object"))
        ) {
            return null
        }

        val fqName = c.fqNameWhenAvailable
        if (fqName == null) {
            return null
        }

        fun tryGetPair(arity: Int): Pair<IrClass, List<IrTypeArgument>?>? {
            val replaced = getClassByFqName(pluginContext, fqName)?.owner ?: return null
            return Pair(
                replaced,
                List(arity) {
                    makeTypeProjection(pluginContext.irBuiltIns.anyNType, Variance.INVARIANT)
                }
            )
        }

        // The list of types handled here match
        // https://github.com/JetBrains/kotlin/blob/d7c7d1efd2c0983c13b175e9e4b1cda979521159/plugins/parcelize/parcelize-compiler/src/org/jetbrains/kotlin/parcelize/ir/AndroidSymbols.kt
        // Specifically, types are added for generic types created in AndroidSymbols.kt.
        // This replacement is from a raw type to its matching parameterized type with `Object` type
        // arguments.
        return when (fqName.asString()) {
            "java.util.ArrayList" -> tryGetPair(1)
            "java.util.LinkedHashMap" -> tryGetPair(2)
            "java.util.LinkedHashSet" -> tryGetPair(1)
            "java.util.List" -> tryGetPair(1)
            "java.util.TreeMap" -> tryGetPair(2)
            "java.util.TreeSet" -> tryGetPair(1)
            "java.lang.Class" -> tryGetPair(1)
            else -> null
        }
    }

    private fun useAnonymousClass(c: IrClass) =
        tw.lm.anonymousTypeMapping.getOrPut(c) {
            TypeResults(
                TypeResult(tw.getFreshIdLabel<DbClassorinterface>(), "", ""),
                TypeResult(fakeKotlinType(), "TODO", "TODO")
            )
        }

    fun fakeKotlinType(): Label<out DbKt_type> {
        val fakeKotlinPackageId: Label<DbPackage> =
            tw.getLabelFor("@\"FakeKotlinPackage\"", { tw.writePackages(it, "fake.kotlin") })
        val fakeKotlinClassId: Label<DbClassorinterface> =
            tw.getLabelFor(
                "@\"FakeKotlinClass\"",
                { tw.writeClasses_or_interfaces(it, "FakeKotlinClass", fakeKotlinPackageId, it) }
            )
        val fakeKotlinTypeId: Label<DbKt_nullable_type> =
            tw.getLabelFor(
                "@\"FakeKotlinType\"",
                { tw.writeKt_nullable_types(it, fakeKotlinClassId) }
            )
        return fakeKotlinTypeId
    }

    // `args` can be null to describe a raw generic type.
    // For non-generic types it will be zero-length list.
    fun useSimpleTypeClass(
        c: IrClass,
        args: List<IrTypeArgument>?,
        hasQuestionMark: Boolean
    ): TypeResults {
        val classInstanceResult = useClassInstance(c, args)
        val javaClassId = classInstanceResult.typeResult.id
        val kotlinQualClassName = getUnquotedClassLabel(c, args).classLabel
        val javaResult = classInstanceResult.typeResult
        val kotlinResult =
            if (true) TypeResult(fakeKotlinType(), "TODO", "TODO")
            else if (hasQuestionMark) {
                val kotlinSignature = "$kotlinQualClassName?" // TODO: Is this right?
                val kotlinLabel = "@\"kt_type;nullable;$kotlinQualClassName\""
                val kotlinId: Label<DbKt_nullable_type> =
                    tw.getLabelFor(kotlinLabel, { tw.writeKt_nullable_types(it, javaClassId) })
                TypeResult(kotlinId, kotlinSignature, "TODO")
            } else {
                val kotlinSignature = kotlinQualClassName // TODO: Is this right?
                val kotlinLabel = "@\"kt_type;notnull;$kotlinQualClassName\""
                val kotlinId: Label<DbKt_notnull_type> =
                    tw.getLabelFor(kotlinLabel, { tw.writeKt_notnull_types(it, javaClassId) })
                TypeResult(kotlinId, kotlinSignature, "TODO")
            }
        return TypeResults(javaResult, kotlinResult)
    }

    // Given either a primitive array or a boxed array, returns primitive arrays unchanged,
    // but returns boxed arrays with a nullable, invariant component type, with any nested arrays
    // similarly transformed. For example, Array<out Array<in E>> would become Array<Array<E?>?>
    // Array<*> will become Array<Any?>.
    private fun getInvariantNullableArrayType(arrayType: IrSimpleType): IrSimpleType =
        if (arrayType.isPrimitiveArray()) arrayType
        else {
            val componentType = arrayType.getArrayElementTypeCodeQL(pluginContext.irBuiltIns)
            val componentTypeBroadened =
                when (componentType) {
                    is IrSimpleType ->
                        if (isArray(componentType)) getInvariantNullableArrayType(componentType)
                        else componentType
                    else -> componentType
                }
            val unchanged =
                componentType == componentTypeBroadened &&
                    (arrayType.arguments[0] as? IrTypeProjection)?.variance == Variance.INVARIANT &&
                    componentType.isNullableCodeQL()
            if (unchanged) arrayType
            else
                IrSimpleTypeImpl(
                    arrayType.classifier,
                    true,
                    listOf(makeTypeProjection(componentTypeBroadened, Variance.INVARIANT)),
                    listOf()
                )
        }

    /*
    Kotlin arrays can be broken down as:

    isArray(t)
    |- t.isBoxedArrayCodeQL
    |  |- t.isArray()         e.g. Array<Boolean>, Array<Boolean?>
    |  |- t.isNullableArray() e.g. Array<Boolean>?, Array<Boolean?>?
    |- t.isPrimitiveArray()   e.g. BooleanArray

    For the corresponding Java types:
    Boxed arrays are represented as e.g. java.lang.Boolean[].
    Primitive arrays are represented as e.g. boolean[].
    */

    private fun isArray(t: IrType) = t.isBoxedArrayCodeQL || t.isPrimitiveArray()

    data class ArrayInfo(
        val elementTypeResults: TypeResults,
        val componentTypeResults: TypeResults,
        val dimensions: Int
    )

    /**
     * `t` is somewhere in a stack of array types, or possibly the element type of the innermost
     * array. For example, in `Array<Array<Int>>`, we will be called with `t` being
     * `Array<Array<Int>>`, then `Array<Int>`, then `Int`. `isPrimitiveArray` is true if we are
     * immediately nested inside a primitive array.
     */
    private fun useArrayType(t: IrType, isPrimitiveArray: Boolean): ArrayInfo {

        if (!isArray(t)) {
            val nullableT = if (t.isPrimitiveType() && !isPrimitiveArray) t.makeNullable() else t
            val typeResults = useType(nullableT)
            return ArrayInfo(typeResults, typeResults, 0)
        }

        if (t !is IrSimpleType) {
            logger.error("Unexpected non-simple array type: ${t.javaClass}")
            return ArrayInfo(extractErrorType(), extractErrorType(), 0)
        }

        val arrayClass = t.classifier.owner
        if (arrayClass !is IrClass) {
            logger.error("Unexpected owner type for array type: ${arrayClass.javaClass}")
            return ArrayInfo(extractErrorType(), extractErrorType(), 0)
        }

        // Because Java's arrays are covariant, Kotlin will render
        // Array<in X> as Object[], Array<Array<in X>> as Object[][] etc.
        val elementType =
            if (
                (t.arguments.singleOrNull() as? IrTypeProjection)?.variance == Variance.IN_VARIANCE
            ) {
                pluginContext.irBuiltIns.anyType
            } else {
                t.getArrayElementTypeCodeQL(pluginContext.irBuiltIns)
            }

        val recInfo = useArrayType(elementType, t.isPrimitiveArray())

        val javaShortName = recInfo.componentTypeResults.javaResult.shortName + "[]"
        val kotlinShortName = recInfo.componentTypeResults.kotlinResult.shortName + "[]"
        val elementTypeLabel = recInfo.elementTypeResults.javaResult.id
        val componentTypeLabel = recInfo.componentTypeResults.javaResult.id
        val dimensions = recInfo.dimensions + 1

        val id =
            tw.getLabelFor<DbArray>("@\"array;$dimensions;{$elementTypeLabel}\"") {
                tw.writeArrays(it, javaShortName, elementTypeLabel, dimensions, componentTypeLabel)

                extractClassSupertypes(
                    arrayClass,
                    it,
                    ExtractSupertypesMode.Specialised(t.arguments)
                )

                // array.length
                val length = tw.getLabelFor<DbField>("@\"field;{$it};length\"")
                val intTypeIds = useType(pluginContext.irBuiltIns.intType)
                tw.writeFields(length, "length", intTypeIds.javaResult.id, it)
                tw.writeFieldsKotlinType(length, intTypeIds.kotlinResult.id)
                addModifiers(length, "public", "final")

                // Note we will only emit one `clone()` method per Java array type, so we choose
                // `Array<C?>` as its Kotlin
                // return type, where C is the component type with any nested arrays themselves
                // invariant and nullable.
                val kotlinCloneReturnType = getInvariantNullableArrayType(t).makeNullable()
                val kotlinCloneReturnTypeLabel = useType(kotlinCloneReturnType).kotlinResult.id

                val clone = tw.getLabelFor<DbMethod>("@\"callable;{$it}.clone(){$it}\"")
                tw.writeMethods(clone, "clone", "clone()", it, it, clone)
                tw.writeMethodsKotlinType(clone, kotlinCloneReturnTypeLabel)
                addModifiers(clone, "public")
            }

        val javaResult =
            TypeResult(id, recInfo.componentTypeResults.javaResult.signature + "[]", javaShortName)
        val kotlinResult =
            TypeResult(
                fakeKotlinType(),
                recInfo.componentTypeResults.kotlinResult.signature + "[]",
                kotlinShortName
            )
        val typeResults = TypeResults(javaResult, kotlinResult)

        return ArrayInfo(recInfo.elementTypeResults, typeResults, dimensions)
    }

    enum class TypeContext {
        RETURN,
        GENERIC_ARGUMENT,
        OTHER
    }

    private fun useSimpleType(s: IrSimpleType, context: TypeContext): TypeResults {
        if (s.abbreviation != null) {
            // TODO: Extract this information
        }
        // We use this when we don't actually have an IrClass for a class
        // we want to refer to
        // TODO: Eliminate the need for this if possible
        fun makeClass(pkgName: String, className: String): Label<DbClassorinterface> {
            val pkgId = extractPackage(pkgName)
            val label = "@\"class;$pkgName.$className\""
            val classId: Label<DbClassorinterface> =
                tw.getLabelFor(label, { tw.writeClasses_or_interfaces(it, className, pkgId, it) })
            return classId
        }
        fun primitiveType(
            kotlinClass: IrClass,
            primitiveName: String?,
            otherIsPrimitive: Boolean,
            javaClass: IrClass,
            kotlinPackageName: String,
            kotlinClassName: String
        ): TypeResults {
            // Note the use of `hasEnhancedNullability` here covers cases like `@NotNull Integer`,
            // which must be extracted as `Integer` not `int`.
            val javaResult =
                if (
                    (context == TypeContext.RETURN ||
                        (context == TypeContext.OTHER && otherIsPrimitive)) &&
                        !s.isNullableCodeQL() &&
                        getKotlinType(s)?.hasEnhancedNullability() != true &&
                        primitiveName != null
                ) {
                    val label: Label<DbPrimitive> =
                        tw.getLabelFor(
                            "@\"type;$primitiveName\"",
                            { tw.writePrimitives(it, primitiveName) }
                        )
                    TypeResult(label, primitiveName, primitiveName)
                } else {
                    addClassLabel(javaClass, listOf())
                }
            val kotlinClassId = useClassInstance(kotlinClass, listOf()).typeResult.id
            val kotlinResult =
                if (true) TypeResult(fakeKotlinType(), "TODO", "TODO")
                else if (s.isNullableCodeQL()) {
                    val kotlinSignature =
                        "$kotlinPackageName.$kotlinClassName?" // TODO: Is this right?
                    val kotlinLabel = "@\"kt_type;nullable;$kotlinPackageName.$kotlinClassName\""
                    val kotlinId: Label<DbKt_nullable_type> =
                        tw.getLabelFor(
                            kotlinLabel,
                            { tw.writeKt_nullable_types(it, kotlinClassId) }
                        )
                    TypeResult(kotlinId, kotlinSignature, "TODO")
                } else {
                    val kotlinSignature =
                        "$kotlinPackageName.$kotlinClassName" // TODO: Is this right?
                    val kotlinLabel = "@\"kt_type;notnull;$kotlinPackageName.$kotlinClassName\""
                    val kotlinId: Label<DbKt_notnull_type> =
                        tw.getLabelFor(kotlinLabel, { tw.writeKt_notnull_types(it, kotlinClassId) })
                    TypeResult(kotlinId, kotlinSignature, "TODO")
                }
            return TypeResults(javaResult, kotlinResult)
        }

        val owner = s.classifier.owner
        val primitiveInfo = primitiveTypeMapping.getPrimitiveInfo(s)

        when {
            primitiveInfo != null -> {
                if (owner is IrClass) {
                    return primitiveType(
                        owner,
                        primitiveInfo.primitiveName,
                        primitiveInfo.otherIsPrimitive,
                        primitiveInfo.javaClass,
                        primitiveInfo.kotlinPackageName,
                        primitiveInfo.kotlinClassName
                    )
                } else {
                    logger.error(
                        "Got primitive info for non-class (${owner.javaClass}) for ${s.render()}"
                    )
                    return extractErrorType()
                }
            }
            (s.isBoxedArrayCodeQL && s.arguments.isNotEmpty()) || s.isPrimitiveArray() -> {
                val arrayInfo = useArrayType(s, false)
                return arrayInfo.componentTypeResults
            }
            owner is IrClass -> {
                val args = if (s.codeQlIsRawType()) null else s.arguments

                return useSimpleTypeClass(owner, args, s.isNullableCodeQL())
            }
            owner is IrTypeParameter -> {
                val javaResult = useTypeParameter(owner)
                val aClassId = makeClass("kotlin", "TypeParam") // TODO: Wrong
                val kotlinResult =
                    if (true) TypeResult(fakeKotlinType(), "TODO", "TODO")
                    else if (s.isNullableCodeQL()) {
                        val kotlinSignature = "${javaResult.signature}?" // TODO: Wrong
                        val kotlinLabel = "@\"kt_type;nullable;type_param\"" // TODO: Wrong
                        val kotlinId: Label<DbKt_nullable_type> =
                            tw.getLabelFor(kotlinLabel, { tw.writeKt_nullable_types(it, aClassId) })
                        TypeResult(kotlinId, kotlinSignature, "TODO")
                    } else {
                        val kotlinSignature = javaResult.signature // TODO: Wrong
                        val kotlinLabel = "@\"kt_type;notnull;type_param\"" // TODO: Wrong
                        val kotlinId: Label<DbKt_notnull_type> =
                            tw.getLabelFor(kotlinLabel, { tw.writeKt_notnull_types(it, aClassId) })
                        TypeResult(kotlinId, kotlinSignature, "TODO")
                    }
                return TypeResults(javaResult, kotlinResult)
            }
            else -> {
                logger.error("Unrecognised IrSimpleType: " + s.javaClass + ": " + s.render())
                return extractErrorType()
            }
        }
    }

    private fun parentOf(d: IrDeclaration): IrDeclarationParent {
        if (d is IrField) {
            return getFieldParent(d)
        }
        return d.parent
    }

    fun useDeclarationParentOf(
        // The declaration
        d: IrDeclaration,
        // Whether the type of entity whose parent this is can be a
        // top-level entity in the JVM's eyes. If so, then its parent may
        // be a file; otherwise, if dp is a file foo.kt, then the parent
        // is really the JVM class FooKt.
        canBeTopLevel: Boolean,
        classTypeArguments: List<IrTypeArgument>? = null,
        inReceiverContext: Boolean = false
    ): Label<out DbElement>? {

        val parent = parentOf(d)
        if (parent is IrExternalPackageFragment) {
            // This is in a file class.
            val fqName = getFileClassFqName(d)
            if (fqName == null) {
                logger.error(
                    "Can't get FqName for declaration in external package fragment ${d.javaClass}"
                )
                return null
            }
            return extractFileClass(fqName)
        }
        return useDeclarationParent(parent, canBeTopLevel, classTypeArguments, inReceiverContext)
    }

    // Generally, useDeclarationParentOf should be used instead of
    // calling this directly, as this cannot handle
    // IrExternalPackageFragment
    fun useDeclarationParent(
        // The declaration parent according to Kotlin
        dp: IrDeclarationParent,
        // Whether the type of entity whose parent this is can be a
        // top-level entity in the JVM's eyes. If so, then its parent may
        // be a file; otherwise, if dp is a file foo.kt, then the parent
        // is really the JVM class FooKt.
        canBeTopLevel: Boolean,
        classTypeArguments: List<IrTypeArgument>? = null,
        inReceiverContext: Boolean = false
    ): Label<out DbElement>? =
        when (dp) {
            is IrFile ->
                if (canBeTopLevel) {
                    usePackage(dp.packageFqName.asString())
                } else {
                    extractFileClass(dp)
                }
            is IrClass ->
                if (classTypeArguments != null) {
                    useClassInstance(dp, classTypeArguments, inReceiverContext).typeResult.id
                } else {
                    val replacedType = tryReplaceParcelizeRawType(dp)
                    if (replacedType == null) {
                        useClassSource(dp)
                    } else {
                        useClassInstance(replacedType.first, replacedType.second, inReceiverContext)
                            .typeResult
                            .id
                    }
                }
            is IrFunction -> useFunction(dp)
            is IrExternalPackageFragment -> {
                logger.error("Unable to handle IrExternalPackageFragment as an IrDeclarationParent")
                null
            }
            else -> {
                logger.error("Unrecognised IrDeclarationParent: " + dp.javaClass)
                null
            }
        }

    private val IrDeclaration.isAnonymousFunction
        get() = this is IrSimpleFunction && name == SpecialNames.NO_NAME_PROVIDED

    data class FunctionNames(val nameInDB: String, val kotlinName: String)

    @OptIn(ObsoleteDescriptorBasedAPI::class)
    private fun getJvmModuleName(f: IrFunction) =
        NameUtils.sanitizeAsJavaIdentifier(
            getJvmModuleNameForDeserializedDescriptor(f.descriptor)
                ?: JvmCodegenUtil.getModuleName(pluginContext.moduleDescriptor)
        )

    fun getFunctionShortName(f: IrFunction): FunctionNames {
        if (f.origin == IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA || f.isAnonymousFunction)
            return FunctionNames(
                OperatorNameConventions.INVOKE.asString(),
                OperatorNameConventions.INVOKE.asString()
            )

        fun getSuffixIfInternal() =
            if (
                f.visibility == DescriptorVisibilities.INTERNAL &&
                    f !is IrConstructor &&
                    !(f.parent is IrFile || isExternalFileClassMember(f))
            ) {
                "\$" + getJvmModuleName(f)
            } else {
                ""
            }

        (f as? IrSimpleFunction)?.correspondingPropertySymbol?.let {
            val propName = it.owner.name.asString()
            val getter = it.owner.getter
            val setter = it.owner.setter

            if (it.owner.parentClassOrNull?.kind == ClassKind.ANNOTATION_CLASS) {
                if (getter == null) {
                    logger.error(
                        "Expected to find a getter for a property inside an annotation class"
                    )
                    return FunctionNames(propName, propName)
                } else {
                    val jvmName = getJvmName(getter)
                    return FunctionNames(jvmName ?: propName, propName)
                }
            }

            val maybeFunctionName =
                when (f) {
                    getter -> JvmAbi.getterName(propName)
                    setter -> JvmAbi.setterName(propName)
                    else -> {
                        logger.error(
                            "Function has a corresponding property, but is neither the getter nor the setter"
                        )
                        null
                    }
                }
            maybeFunctionName?.let { defaultFunctionName ->
                val suffix =
                    if (
                        f.visibility == DescriptorVisibilities.PRIVATE &&
                            f.origin == IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR
                    ) {
                        "\$private"
                    } else {
                        getSuffixIfInternal()
                    }
                return FunctionNames(
                    getJvmName(f) ?: "$defaultFunctionName$suffix",
                    defaultFunctionName
                )
            }
        }
        return FunctionNames(
            getJvmName(f) ?: "${f.name.asString()}${getSuffixIfInternal()}",
            f.name.asString()
        )
    }

    // This excludes class type parameters that show up in (at least) constructors' typeParameters
    // list.
    fun getFunctionTypeParameters(f: IrFunction): List<IrTypeParameter> {
        return if (f is IrConstructor) f.typeParameters
        else f.typeParameters.filter { it.parent == f }
    }

    private fun getTypeParameters(dp: IrDeclarationParent): List<IrTypeParameter> =
        when (dp) {
            is IrClass -> dp.typeParameters
            is IrFunction -> getFunctionTypeParameters(dp)
            else -> listOf()
        }

    private fun getEnclosingClass(it: IrDeclarationParent): IrClass? =
        when (it) {
            is IrClass -> it
            is IrFunction -> getEnclosingClass(it.parent)
            else -> null
        }

    val javaUtilCollection by lazy { referenceExternalClass("java.util.Collection") }

    val wildcardCollectionType by lazy {
        javaUtilCollection?.let { it.symbol.typeWithArguments(listOf(IrStarProjectionImpl)) }
    }

    private fun makeCovariant(t: IrTypeArgument) =
        t.typeOrNull?.let { makeTypeProjection(it, Variance.OUT_VARIANCE) } ?: t

    private fun makeArgumentsCovariant(t: IrType) =
        (t as? IrSimpleType)?.let {
            t.toBuilder()
                .also { b -> b.arguments = b.arguments.map(this::makeCovariant) }
                .buildSimpleType()
        } ?: t

    fun eraseCollectionsMethodParameterType(
        t: IrType,
        collectionsMethodName: String,
        paramIdx: Int
    ) =
        when (collectionsMethodName) {
            "contains",
            "remove",
            "containsKey",
            "containsValue",
            "get",
            "indexOf",
            "lastIndexOf" -> javaLangObjectType
            "getOrDefault" -> if (paramIdx == 0) javaLangObjectType else null
            "containsAll",
            "removeAll",
            "retainAll" -> wildcardCollectionType
            // Kotlin defines these like addAll(Collection<E>); Java uses addAll(Collection<?
            // extends E>)
            "putAll",
            "addAll" -> makeArgumentsCovariant(t)
            else -> null
        } ?: t

    private fun overridesFunctionDefinedOn(f: IrFunction, packageName: String, className: String) =
        (f as? IrSimpleFunction)?.let {
            it.overriddenSymbols.any { overridden ->
                overridden.owner.parentClassOrNull?.let { defnClass ->
                    defnClass.name.asString() == className &&
                        defnClass.packageFqName?.asString() == packageName
                } ?: false
            }
        } ?: false

    @OptIn(ObsoleteDescriptorBasedAPI::class)
    fun overridesCollectionsMethodWithAlteredParameterTypes(f: IrFunction) =
        BuiltinMethodsWithSpecialGenericSignature
            .getOverriddenBuiltinFunctionWithErasedValueParametersInJava(f.descriptor) != null ||
            (f.name.asString() == "putAll" &&
                overridesFunctionDefinedOn(f, "kotlin.collections", "MutableMap")) ||
            (f.name.asString() == "addAll" &&
                overridesFunctionDefinedOn(f, "kotlin.collections", "MutableCollection")) ||
            (f.name.asString() == "addAll" &&
                overridesFunctionDefinedOn(f, "kotlin.collections", "MutableList"))

    private val jvmWildcardAnnotation = FqName("kotlin.jvm.JvmWildcard")
    private val jvmWildcardSuppressionAnnotation = FqName("kotlin.jvm.JvmSuppressWildcards")

    private fun arrayExtendsAdditionAllowed(t: IrSimpleType): Boolean =
        // Note the array special case includes Array<*>, which does permit adding `? extends ...`
        // (making `? extends Object[]` in that case)
        // Surprisingly Array<in X> does permit this as well, though the contravariant array lowers
        // to Object[] so this ends up `? extends Object[]` as well.
        t.arguments[0].let {
            when (it) {
                is IrTypeProjection ->
                    when (it.variance) {
                        Variance.INVARIANT -> false
                        Variance.IN_VARIANCE -> !(it.type.isAny() || it.type.isNullableAny())
                        Variance.OUT_VARIANCE -> extendsAdditionAllowed(it.type)
                    }
                else -> true
            }
        }

    private fun extendsAdditionAllowed(t: IrType) =
        if (t.isBoxedArrayCodeQL) {
            if (t is IrSimpleType) {
                arrayExtendsAdditionAllowed(t)
            } else {
                logger.warn("Boxed array of unexpected kind ${t.javaClass}")
                // Return false, for no particular reason
                false
            }
        } else {
            ((t as? IrSimpleType)?.classOrNull?.owner?.isFinalClass) != true
        }

    private fun wildcardAdditionAllowed(
        v: Variance,
        t: IrType,
        addByDefault: Boolean,
        javaVariance: Variance?
    ) =
        when {
            t.hasAnnotation(jvmWildcardAnnotation) -> true
            // If a Java declaration specifies a variance, introduce it even if it's pointless (e.g.
            // ? extends FinalClass, or ? super Object)
            javaVariance == v -> true
            !addByDefault -> false
            v == Variance.IN_VARIANCE -> !(t.isNullableAny() || t.isAny())
            v == Variance.OUT_VARIANCE -> extendsAdditionAllowed(t)
            else -> false
        }

    // Returns true if `t` has `@JvmSuppressWildcards` or `@JvmSuppressWildcards(true)`,
    // false if it has `@JvmSuppressWildcards(false)`,
    // and null if the annotation is not present.
    @Suppress("UNCHECKED_CAST")
    private fun getWildcardSuppressionDirective(t: IrAnnotationContainer): Boolean? =
        t.getAnnotation(jvmWildcardSuppressionAnnotation)?.let {
            @Suppress("USELESS_CAST") // `as? Boolean` is not needed for Kotlin < 2.1
            (it.getValueArgument(0) as? CodeQLIrConst<Boolean>)?.value as? Boolean ?: true
        }

    private fun addJavaLoweringArgumentWildcards(
        p: IrTypeParameter,
        t: IrTypeArgument,
        addByDefault: Boolean,
        javaType: JavaType?
    ): IrTypeArgument =
        (t as? IrTypeProjection)?.let {
            val newAddByDefault = getWildcardSuppressionDirective(it.type)?.not() ?: addByDefault
            val newBase = addJavaLoweringWildcards(it.type, newAddByDefault, javaType)
            // Note javaVariance == null means we don't have a Java type to conform to -- for
            // example if this is a Kotlin source definition.
            val javaVariance =
                javaType?.let { jType ->
                    when (jType) {
                        is JavaWildcardType ->
                            if (jType.isExtends) Variance.OUT_VARIANCE else Variance.IN_VARIANCE
                        else -> Variance.INVARIANT
                    }
                }
            val newVariance =
                if (
                    it.variance == Variance.INVARIANT &&
                        p.variance != Variance.INVARIANT &&
                        // The next line forbids inferring a wildcard type when we have a
                        // corresponding Java type with conflicting variance.
                        // For example, Java might declare f(Comparable<CharSequence> cs), in which
                        // case we shouldn't add a `? super ...`
                        // wildcard. Note if javaType is unknown (e.g. this is a Kotlin source
                        // element), we assume wildcards should be added.
                        (javaVariance == null || javaVariance == p.variance) &&
                        wildcardAdditionAllowed(p.variance, it.type, newAddByDefault, javaVariance)
                )
                    p.variance
                else it.variance
            if (newBase !== it.type || newVariance != it.variance)
                makeTypeProjection(newBase, newVariance)
            else null
        } ?: t

    private fun getJavaTypeArgument(jt: JavaType, idx: Int): JavaType? =
        when (jt) {
            is JavaWildcardType -> jt.bound?.let { getJavaTypeArgument(it, idx) }
            is JavaClassifierType -> jt.typeArguments.getOrNull(idx)
            is JavaArrayType -> if (idx == 0) jt.componentType else null
            else -> null
        }

    fun addJavaLoweringWildcards(t: IrType, addByDefault: Boolean, javaType: JavaType?): IrType =
        (t as? IrSimpleType)?.let {
            val newAddByDefault = getWildcardSuppressionDirective(t)?.not() ?: addByDefault
            val typeParams = it.classOrNull?.owner?.typeParameters ?: return t
            val newArgs =
                typeParams.zip(it.arguments).mapIndexed { idx, pair ->
                    addJavaLoweringArgumentWildcards(
                        pair.first,
                        pair.second,
                        newAddByDefault,
                        javaType?.let { jt -> getJavaTypeArgument(jt, idx) }
                    )
                }
            return if (newArgs.zip(it.arguments).all { pair -> pair.first === pair.second }) t
            else it.toBuilder().also { builder -> builder.arguments = newArgs }.buildSimpleType()
        } ?: t

    /*
     * This is the normal getFunctionLabel function to use. If you want
     * to refer to the function in its source class then
     * classTypeArgsIncludingOuterClasses should be null. Otherwise, it
     * is the list of type arguments that need to be applied to its
     * enclosing classes to get the instantiation that this function is
     * in.
     */
    fun getFunctionLabel(
        f: IrFunction,
        classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?
    ): String? {
        val parentId = useDeclarationParentOf(f, false, classTypeArgsIncludingOuterClasses, true)
        if (parentId == null) {
            logger.error("Couldn't get parent ID for function label")
            return null
        }
        return getFunctionLabel(f, parentId, classTypeArgsIncludingOuterClasses)
    }

    /*
     * There are some pairs of classes (e.g. `kotlin.Throwable` and
     * `java.lang.Throwable`) which are really just 2 different names
     * for the same class. However, we extract them as separate
     * classes. When extracting `kotlin.Throwable`'s methods, if we
     * looked up the parent ID ourselves, we would get as ID for
     * `java.lang.Throwable`, which isn't what we want. So we have to
     * allow it to be passed in.
     *
     * `maybeParameterList` can be supplied to override the function's
     * value parameters; this is used for generating labels of overloads
     * that omit one or more parameters that has a default value specified.
     */
    @OptIn(ObsoleteDescriptorBasedAPI::class)
    fun getFunctionLabel(
        f: IrFunction,
        parentId: Label<out DbElement>,
        classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?,
        maybeParameterList: List<IrValueParameter>? = null
    ): String =
        getFunctionLabel(
            f.parent,
            parentId,
            getFunctionShortName(f).nameInDB,
            (maybeParameterList ?: f.valueParameters).map { it.type },
            getAdjustedReturnType(f),
            f.extensionReceiverParameter?.type,
            getFunctionTypeParameters(f),
            classTypeArgsIncludingOuterClasses,
            overridesCollectionsMethodWithAlteredParameterTypes(f),
            getJavaCallable(f),
            !getInnermostWildcardSupppressionAnnotation(f)
        )

    /*
     * This function actually generates the label for a function.
     * Sometimes, a function is only generated by kotlinc when writing a
     * class file, so there is no corresponding `IrFunction` for it.
     * This function therefore takes all the constituent parts of a
     * function instead.
     */
    fun getFunctionLabel(
        // The parent of the function; normally f.parent.
        parent: IrDeclarationParent,
        // The ID of the function's parent, or null if we should work it out ourselves.
        parentId: Label<out DbElement>,
        // The name of the function; normally f.name.asString().
        name: String,
        // The types of the value parameters that the functions takes; normally
        // f.valueParameters.map { it.type }.
        parameterTypes: List<IrType>,
        // The return type of the function; normally f.returnType.
        returnType: IrType,
        // The extension receiver of the function, if any; normally
        // f.extensionReceiverParameter?.type.
        extensionParamType: IrType?,
        // The type parameters of the function. This does not include type parameters of enclosing
        // classes.
        functionTypeParameters: List<IrTypeParameter>,
        // The type arguments of enclosing classes of the function.
        classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?,
        // If true, this method implements a Java Collections interface (Collection, Map or List)
        // and may need
        // parameter erasure to match the way this class will appear to an external consumer of the
        // .class file.
        overridesCollectionsMethod: Boolean,
        // The Java signature of this callable, if known.
        javaSignature: JavaMember?,
        // If true, Java wildcards implied by Kotlin type parameter variance should be added by
        // default to this function's value parameters' types.
        // (Return-type wildcard addition is always off by default)
        addParameterWildcardsByDefault: Boolean,
        // The prefix used in the label. "callable", unless a property label is created, then it's
        // "property".
        prefix: String = "callable"
    ): String {
        val allParamTypes =
            if (extensionParamType == null) parameterTypes
            else listOf(extensionParamType) + parameterTypes

        val substitutionMap =
            classTypeArgsIncludingOuterClasses?.let { notNullArgs ->
                if (notNullArgs.isEmpty()) {
                    null
                } else {
                    val enclosingClass = getEnclosingClass(parent)
                    enclosingClass?.let { notNullClass ->
                        makeTypeGenericSubstitutionMap(notNullClass, notNullArgs)
                    }
                }
            }
        val getIdForFunctionLabel = { it: IndexedValue<IrType> ->
            // Kotlin rewrites certain Java collections types adding additional generic
            // constraints-- for example,
            // Collection.remove(Object) because Collection.remove(Collection::E) in the Kotlin
            // universe.
            // If this has happened, erase the type again to get the correct Java signature.
            val maybeAmendedForCollections =
                if (overridesCollectionsMethod)
                    eraseCollectionsMethodParameterType(it.value, name, it.index)
                else it.value
            // Add any wildcard types that the Kotlin compiler would add in the Java lowering of
            // this function:
            val withAddedWildcards =
                addJavaLoweringWildcards(
                    maybeAmendedForCollections,
                    addParameterWildcardsByDefault,
                    javaSignature?.let { sig -> getJavaValueParameterType(sig, it.index) }
                )
            // Now substitute any class type parameters in:
            val maybeSubbed =
                withAddedWildcards.substituteTypeAndArguments(
                    substitutionMap,
                    TypeContext.OTHER,
                    pluginContext
                )
            // Finally, mimic the Java extractor's behaviour by naming functions with type
            // parameters for their erased types;
            // those without type parameters are named for the generic type.
            val maybeErased =
                if (functionTypeParameters.isEmpty()) maybeSubbed else erase(maybeSubbed)
            "{${useType(maybeErased).javaResult.id}}"
        }
        val paramTypeIds =
            allParamTypes
                .withIndex()
                .joinToString(separator = ",", transform = getIdForFunctionLabel)
        val labelReturnType =
            if (name == "<init>") pluginContext.irBuiltIns.unitType
            else
                erase(
                    returnType.substituteTypeAndArguments(
                        substitutionMap,
                        TypeContext.RETURN,
                        pluginContext
                    )
                )
        // Note that `addJavaLoweringWildcards` is not required here because the return type used to
        // form the function
        // label is always erased.
        val returnTypeId = useType(labelReturnType, TypeContext.RETURN).javaResult.id
        // This suffix is added to generic methods (and constructors) to match the Java extractor's
        // behaviour.
        // Comments in that extractor indicates it didn't want the label of the callable to clash
        // with the raw
        // method (and presumably that disambiguation is never needed when the method belongs to a
        // parameterized
        // instance of a generic class), but as of now I don't know when the raw method would be
        // referred to.
        val typeArgSuffix =
            if (
                functionTypeParameters.isNotEmpty() &&
                    classTypeArgsIncludingOuterClasses.isNullOrEmpty()
            )
                "<${functionTypeParameters.size}>"
            else ""
        return "@\"$prefix;{$parentId}.$name($paramTypeIds){$returnTypeId}$typeArgSuffix\""
    }

    val javaLangClass by lazy { referenceExternalClass("java.lang.Class") }

    fun kClassToJavaClass(t: IrType): IrType {
        when (t) {
            is IrSimpleType -> {
                if (t.classifier == pluginContext.irBuiltIns.kClassClass) {
                    javaLangClass?.let { jlc ->
                        return jlc.symbol.typeWithArguments(t.arguments)
                    }
                } else {
                    t.classOrNull?.let { tCls ->
                        if (t.isBoxedArrayCodeQL) {
                            (t.arguments.singleOrNull() as? IrTypeProjection)?.let { elementTypeArg
                                ->
                                val elementType = elementTypeArg.type
                                val replacedElementType = kClassToJavaClass(elementType)
                                if (replacedElementType !== elementType) {
                                    val newArg =
                                        makeTypeProjection(
                                            replacedElementType,
                                            elementTypeArg.variance
                                        )
                                    return tCls
                                        .typeWithArguments(listOf(newArg))
                                        .codeQlWithHasQuestionMark(t.isNullableCodeQL())
                                }
                            }
                        }
                    }
                }
            }
            is IrDynamicType -> {}
            is IrErrorType -> {}
        }
        return t
    }

    fun isAnnotationClassField(f: IrField) =
        f.correspondingPropertySymbol?.let { isAnnotationClassProperty(it) } ?: false

    private fun isAnnotationClassProperty(p: IrPropertySymbol) =
        p.owner.parentClassOrNull?.kind == ClassKind.ANNOTATION_CLASS

    fun getAdjustedReturnType(f: IrFunction): IrType {
        // Replace annotation val accessor types as needed:
        (f as? IrSimpleFunction)?.correspondingPropertySymbol?.let {
            if (isAnnotationClassProperty(it) && f == it.owner.getter) {
                val replaced = kClassToJavaClass(f.returnType)
                if (replaced != f.returnType) return replaced
            }
        }

        // The return type of `java.util.concurrent.ConcurrentHashMap<K,V>.keySet/0` is defined as
        // `Set<K>` in the stubs inside the Android SDK.
        // This does not match the Java SDK return type: `ConcurrentHashMap.KeySetView<K,V>`, so
        // it's adjusted here.
        // This is a deliberate change in the Android SDK:
        // https://github.com/AndroidSDKSources/android-sdk-sources-for-api-level-31/blob/2c56b25f619575bea12f9c5520ed2259620084ac/java/util/concurrent/ConcurrentHashMap.java#L1244-L1249
        // The annotation on the source is not visible in the android.jar, so we can't make the
        // change based on that.
        // TODO: there are other instances of `dalvik.annotation.codegen.CovariantReturnType` in the
        // Android SDK, we should handle those too if they cause DB inconsistencies
        val parentClass = f.parentClassOrNull
        if (
            parentClass == null ||
                parentClass.fqNameWhenAvailable?.asString() !=
                    "java.util.concurrent.ConcurrentHashMap" ||
                getFunctionShortName(f).nameInDB != "keySet" ||
                f.valueParameters.isNotEmpty() ||
                f.returnType.classFqName?.asString() != "kotlin.collections.MutableSet"
        ) {
            return f.returnType
        }

        val otherKeySet =
            parentClass.declarations.findSubType<IrFunction> {
                it.name.asString() == "keySet" && it.valueParameters.size == 1
            } ?: return f.returnType

        return otherKeySet.returnType.codeQlWithHasQuestionMark(false)
    }

    @OptIn(ObsoleteDescriptorBasedAPI::class)
    fun getJavaCallable(f: IrFunction) =
        (f.descriptor.source as? JavaSourceElement)?.javaElement as? JavaMember

    fun getJavaValueParameterType(m: JavaMember, idx: Int) =
        when (m) {
            is JavaMethod -> m.valueParameters[idx].type
            is JavaConstructor -> m.valueParameters[idx].type
            else -> null
        }

    fun getInnermostWildcardSupppressionAnnotation(d: IrDeclaration) =
        getWildcardSuppressionDirective(d)
            ?:
            // Note not using `parentsWithSelf` as that only works if `d` is an IrDeclarationParent
            d.parents
                .filterIsInstance<IrAnnotationContainer>()
                .mapNotNull { getWildcardSuppressionDirective(it) }
                .firstOrNull()
            ?: false

    /**
     * Class to hold labels for generated classes around local functions, lambdas, function
     * references, and property references.
     */
    open class GeneratedClassLabels(
        val type: TypeResults,
        val constructor: Label<DbConstructor>,
        val constructorBlock: Label<DbBlock>
    )

    /**
     * Class to hold labels generated for locally visible functions, such as
     * - local functions,
     * - lambdas, and
     * - wrappers around function references.
     */
    class LocallyVisibleFunctionLabels(
        type: TypeResults,
        constructor: Label<DbConstructor>,
        constructorBlock: Label<DbBlock>,
        val function: Label<DbMethod>
    ) : GeneratedClassLabels(type, constructor, constructorBlock)

    /**
     * Gets the labels for functions belonging to
     * - local functions, and
     * - lambdas.
     */
    fun getLocallyVisibleFunctionLabels(f: IrFunction): LocallyVisibleFunctionLabels {
        if (!f.isLocalFunction()) {
            logger.error("Extracting a non-local function as a local one")
        }

        var res = tw.lm.locallyVisibleFunctionLabelMapping[f]
        if (res == null) {
            val javaResult = TypeResult(tw.getFreshIdLabel<DbClassorinterface>(), "", "")
            val kotlinResult = TypeResult(tw.getFreshIdLabel<DbKt_notnull_type>(), "", "")
            tw.writeKt_notnull_types(kotlinResult.id, javaResult.id)
            res =
                LocallyVisibleFunctionLabels(
                    TypeResults(javaResult, kotlinResult),
                    tw.getFreshIdLabel(),
                    tw.getFreshIdLabel(),
                    tw.getFreshIdLabel()
                )
            tw.lm.locallyVisibleFunctionLabelMapping[f] = res
        }

        return res
    }

    fun getExistingLocallyVisibleFunctionLabel(f: IrFunction): Label<DbMethod>? {
        if (!f.isLocalFunction()) {
            return null
        }

        return tw.lm.locallyVisibleFunctionLabelMapping[f]?.function
    }

    private fun kotlinFunctionToJavaEquivalent(f: IrFunction, noReplace: Boolean): IrFunction =
        if (noReplace) f
        else
            f.parentClassOrNull?.let { parentClass ->
                getJavaEquivalentClass(parentClass)?.let { javaClass ->
                    if (javaClass != parentClass) {
                        var jvmName = getFunctionShortName(f).nameInDB
                        if (
                            f.name.asString() == "get" &&
                                parentClass.fqNameWhenAvailable?.asString() == "kotlin.String"
                        ) {
                            // `kotlin.String.get` has an equivalent `java.lang.String.get`, which
                            // in turn will be stored in the DB as `java.lang.String.charAt`.
                            // Maybe all operators should be handled the same way, but so far I only
                            // found this case that needed to be special cased. This is the
                            // only operator in `JvmNames.specialFunctions`
                            jvmName = "get"
                        }
                        // Look for an exact type match...
                        javaClass.declarations.findSubType<IrFunction> { decl ->
                            !decl.isFakeOverride &&
                                decl.name.asString() == jvmName &&
                                decl.valueParameters.size == f.valueParameters.size &&
                                decl.valueParameters.zip(f.valueParameters).all { p ->
                                    erase(p.first.type).classifierOrNull ==
                                        erase(p.second.type).classifierOrNull
                                }
                        }
                            ?:
                            // Or check property accessors:
                            (f.propertyIfAccessor as? IrProperty)?.let { kotlinProp ->
                                val javaProp =
                                    javaClass.declarations.findSubType<IrProperty> { decl ->
                                        decl.name == kotlinProp.name
                                    }
                                if (javaProp?.getter?.name == f.name) javaProp.getter
                                else if (javaProp?.setter?.name == f.name) javaProp.setter else null
                            }
                            ?: run {
                                val parentFqName = parentClass.fqNameWhenAvailable?.asString()
                                logger.warn(
                                    "Couldn't find a Java equivalent function to $parentFqName.${f.name.asString()} in ${javaClass.fqNameWhenAvailable?.asString()}"
                                )
                                null
                            }
                    } else null
                }
            } ?: f

    fun isPrivate(d: IrDeclaration) =
        when (d) {
            is IrDeclarationWithVisibility ->
                d.visibility.let {
                    it == DescriptorVisibilities.PRIVATE ||
                        it == DescriptorVisibilities.PRIVATE_TO_THIS
                }
            else -> false
        }

    fun <T : DbCallable> useFunction(
        f: IrFunction,
        classTypeArgsIncludingOuterClasses: List<IrTypeArgument>? = null,
        noReplace: Boolean = false
    ): Label<out T>? {
        if (f.isLocalFunction()) {
            val ids = getLocallyVisibleFunctionLabels(f)
            return ids.function.cast<T>()
        }
        val javaFun = kotlinFunctionToJavaEquivalent(f, noReplace)
        val parentId =
            useDeclarationParentOf(javaFun, false, classTypeArgsIncludingOuterClasses, true)
        if (parentId == null) {
            logger.error("Couldn't find parent ID for function ${f.name.asString()}")
            return null
        }
        return useFunction(f, javaFun, parentId, classTypeArgsIncludingOuterClasses)
    }

    fun <T : DbCallable> useFunction(
        f: IrFunction,
        parentId: Label<out DbElement>,
        classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?,
        noReplace: Boolean = false
    ): Label<out T> {
        if (f.isLocalFunction()) {
            val ids = getLocallyVisibleFunctionLabels(f)
            return ids.function.cast<T>()
        }
        val javaFun = kotlinFunctionToJavaEquivalent(f, noReplace)
        return useFunction(f, javaFun, parentId, classTypeArgsIncludingOuterClasses)
    }

    private fun <T : DbCallable> useFunction(
        f: IrFunction,
        javaFun: IrFunction,
        parentId: Label<out DbElement>,
        classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?
    ): Label<out T> {
        val label = getFunctionLabel(javaFun, parentId, classTypeArgsIncludingOuterClasses)
        val id: Label<T> =
            tw.getLabelFor(label) {
                extractPrivateSpecialisedDeclaration(f, classTypeArgsIncludingOuterClasses)
            }
        if (isExternalDeclaration(javaFun)) {
            extractFunctionLaterIfExternalFileMember(javaFun)
            extractExternalEnclosingClassLater(javaFun)
        }
        return id
    }

    private fun extractPrivateSpecialisedDeclaration(
        d: IrDeclaration,
        classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?
    ) {
        // Note here `classTypeArgsIncludingOuterClasses` being null doesn't signify a raw receiver
        // type but rather that no type args were supplied.
        // This is because a call to a private method can only be observed inside Kotlin code, and
        // Kotlin can't represent raw types.
        if (
            this is KotlinFileExtractor &&
                isPrivate(d) &&
                classTypeArgsIncludingOuterClasses != null &&
                classTypeArgsIncludingOuterClasses.isNotEmpty()
        ) {
            d.parent.let {
                when (it) {
                    is IrClass ->
                        this.extractDeclarationPrototype(
                            d,
                            useClassInstance(it, classTypeArgsIncludingOuterClasses).typeResult.id,
                            classTypeArgsIncludingOuterClasses
                        )
                    else ->
                        logger.warnElement(
                            "Unable to extract specialised declaration that isn't a member of a class",
                            d
                        )
                }
            }
        }
    }

    fun getTypeArgumentLabel(arg: IrTypeArgument): TypeResultWithoutSignature<DbReftype> {

        fun extractBoundedWildcard(
            wildcardKind: Int,
            wildcardLabelStr: String,
            wildcardShortName: String,
            boundLabel: Label<out DbReftype>
        ): Label<DbWildcard> =
            tw.getLabelFor(wildcardLabelStr) { wildcardLabel ->
                tw.writeWildcards(wildcardLabel, wildcardShortName, wildcardKind)
                tw.writeHasLocation(wildcardLabel, tw.unknownLocation)
                tw.getLabelFor<DbTypebound>("@\"bound;0;{$wildcardLabel}\"") {
                    tw.writeTypeBounds(it, boundLabel, 0, wildcardLabel)
                }
            }

        // Note this function doesn't return a signature because type arguments are never
        // incorporated into function signatures.
        return when (arg) {
            is IrStarProjection -> {
                val anyTypeLabel =
                    useType(pluginContext.irBuiltIns.anyType).javaResult.id.cast<DbReftype>()
                TypeResultWithoutSignature(
                    extractBoundedWildcard(1, "@\"wildcard;\"", "?", anyTypeLabel),
                    Unit,
                    "?"
                )
            }
            is IrTypeProjection -> {
                val boundResults = useType(arg.type, TypeContext.GENERIC_ARGUMENT)
                val boundLabel = boundResults.javaResult.id.cast<DbReftype>()

                if (arg.variance == Variance.INVARIANT)
                    boundResults.javaResult.cast<DbReftype>().forgetSignature()
                else {
                    val keyPrefix = if (arg.variance == Variance.IN_VARIANCE) "super" else "extends"
                    val wildcardKind = if (arg.variance == Variance.IN_VARIANCE) 2 else 1
                    val wildcardShortName = "? $keyPrefix ${boundResults.javaResult.shortName}"
                    TypeResultWithoutSignature(
                        extractBoundedWildcard(
                            wildcardKind,
                            "@\"wildcard;$keyPrefix{$boundLabel}\"",
                            wildcardShortName,
                            boundLabel
                        ),
                        Unit,
                        wildcardShortName
                    )
                }
            }
            else -> {
                logger.error("Unexpected type argument.")
                extractJavaErrorType().forgetSignature()
            }
        }
    }

    data class ClassLabelResults(val classLabel: String, val shortName: String)

    /**
     * This returns the `X` in c's label `@"class;X"`.
     *
     * `argsIncludingOuterClasses` can be null to describe a raw generic type. For non-generic types
     * it will be zero-length list.
     */
    private fun getUnquotedClassLabel(
        c: IrClass,
        argsIncludingOuterClasses: List<IrTypeArgument>?
    ): ClassLabelResults {
        val pkg = c.packageFqName?.asString() ?: ""
        val cls = c.name.asString()
        val label =
            if (c.isAnonymousObject) "{${useAnonymousClass(c).javaResult.id}}"
            else
                when (val parent = c.parent) {
                    is IrClass -> {
                        "${getUnquotedClassLabel(parent, listOf()).classLabel}\$$cls"
                    }
                    is IrFunction -> {
                        "{${useFunction<DbMethod>(parent)}}.$cls"
                    }
                    is IrField -> {
                        "{${useField(parent)}}.$cls"
                    }
                    else -> {
                        if (pkg.isEmpty()) cls else "$pkg.$cls"
                    }
                }

        val reorderedArgs = orderTypeArgsLeftToRight(c, argsIncludingOuterClasses)
        val typeArgLabels = reorderedArgs?.map { getTypeArgumentLabel(it) }
        val typeArgsShortName =
            if (typeArgLabels == null) "<>"
            else if (typeArgLabels.isEmpty()) ""
            else
                typeArgLabels.takeLast(c.typeParameters.size).joinToString(
                    prefix = "<",
                    postfix = ">",
                    separator = ","
                ) {
                    it.shortName
                }
        val shortNamePrefix = if (c.isAnonymousObject) "" else cls

        return ClassLabelResults(
            label + (typeArgLabels?.joinToString(separator = "") { ";{${it.id}}" } ?: "<>"),
            shortNamePrefix + typeArgsShortName
        )
    }

    // `args` can be null to describe a raw generic type.
    // For non-generic types it will be zero-length list.
    fun getClassLabel(
        c: IrClass,
        argsIncludingOuterClasses: List<IrTypeArgument>?
    ): ClassLabelResults {
        val unquotedLabel = getUnquotedClassLabel(c, argsIncludingOuterClasses)
        return ClassLabelResults("@\"class;${unquotedLabel.classLabel}\"", unquotedLabel.shortName)
    }

    fun useClassSource(c: IrClass): Label<out DbClassorinterface> {
        // For source classes, the label doesn't include any type arguments
        val classTypeResult = addClassLabel(c, listOf())
        return classTypeResult.id
    }

    fun getTypeParameterParentLabel(param: IrTypeParameter) =
        param.parent.let {
            when (it) {
                is IrClass -> useClassSource(it)
                is IrFunction ->
                    (if (this is KotlinFileExtractor)
                        this.declarationStack
                            .findOverriddenAttributes(it)
                            ?.takeUnless {
                                // When extracting the `static fun f$default(...)` that accompanies
                                // `fun <T> f(val x: T? = defaultExpr, ...)`,
                                // `f$default` has no type parameters, and so there is no
                                // `f$default::T` to refer to.
                                // We have no good way to extract references to `T` in
                                // `defaultExpr`, so we just fall back on describing it
                                // in terms of `f::T`, even though that type variable ought to be
                                // out of scope here.
                                attribs ->
                                attribs.typeParameters?.isEmpty() == true
                            }
                            ?.id
                    else null) ?: useFunction(it, noReplace = true)
                else -> {
                    logger.error("Unexpected type parameter parent $it")
                    null
                }
            }
        }

    fun getTypeParameterLabel(param: IrTypeParameter): String {
        // Use this instead of `useDeclarationParent` so we can use useFunction with noReplace =
        // true,
        // ensuring that e.g. a method-scoped type variable declared on kotlin.String.transform <R>
        // gets
        // a different name to the corresponding java.lang.String.transform <R>, even though
        // useFunction
        // will usually replace references to one function with the other.
        val parentLabel = getTypeParameterParentLabel(param)
        return "@\"typevar;{$parentLabel};${param.name}\""
    }

    private fun useTypeParameter(param: IrTypeParameter) =
        TypeResult(
            tw.getLabelFor<DbTypevariable>(getTypeParameterLabel(param)),
            useType(eraseTypeParameter(param)).javaResult.signature,
            param.name.asString()
        )

    private fun extractModifier(m: String): Label<DbModifier> {
        val modifierLabel = "@\"modifier;$m\""
        val id: Label<DbModifier> = tw.getLabelFor(modifierLabel, { tw.writeModifiers(it, m) })
        return id
    }

    fun addModifiers(modifiable: Label<out DbModifiable>, vararg modifiers: String) =
        modifiers.forEach { tw.writeHasModifier(modifiable, extractModifier(it)) }

    sealed class ExtractSupertypesMode {
        object Unbound : ExtractSupertypesMode()

        object Raw : ExtractSupertypesMode()

        data class Specialised(val typeArgs: List<IrTypeArgument>) : ExtractSupertypesMode()
    }

    /**
     * Extracts the supertypes of class `c`, either the unbound version, raw version or a
     * specialisation to particular type arguments, depending on the value of `mode`. `id` is the
     * label of this class or class instantiation.
     *
     * For example, for type `List` if `mode` `Specialised([String])` then we will extract the
     * supertypes of `List<String>`, i.e. `Appendable<String>` etc, or if `mode` is `Unbound` we
     * will extract `Appendable<E>` where `E` is the type variable declared as `List<E>`. Finally if
     * `mode` is `Raw` we will extract the raw type `Appendable`, represented in QL as
     * `Appendable<>`.
     *
     * Argument `inReceiverContext` will be passed onto the `useClassInstance` invocation for each
     * supertype.
     */
    fun extractClassSupertypes(
        c: IrClass,
        id: Label<out DbReftype>,
        mode: ExtractSupertypesMode = ExtractSupertypesMode.Unbound,
        inReceiverContext: Boolean = false
    ) {
        extractClassSupertypes(
            c.superTypes,
            c.typeParameters,
            id,
            c.isInterfaceLike,
            mode,
            inReceiverContext
        )
    }

    fun extractClassSupertypes(
        superTypes: List<IrType>,
        typeParameters: List<IrTypeParameter>,
        id: Label<out DbReftype>,
        isInterface: Boolean,
        mode: ExtractSupertypesMode = ExtractSupertypesMode.Unbound,
        inReceiverContext: Boolean = false
    ) {
        // Note we only need to substitute type args here because it is illegal to directly extend a
        // type variable.
        // (For example, we can't have `class A<E> : E`, but can have `class A<E> : Comparable<E>`)
        val subbedSupertypes =
            when (mode) {
                is ExtractSupertypesMode.Specialised -> {
                    superTypes.map { it.substituteTypeArguments(typeParameters, mode.typeArgs) }
                }
                else -> superTypes
            }

        for (t in subbedSupertypes) {
            when (t) {
                is IrSimpleType -> {
                    when (val owner = t.classifier.owner) {
                        is IrClass -> {
                            val typeArgs =
                                if (t.arguments.isNotEmpty() && mode is ExtractSupertypesMode.Raw)
                                    null
                                else t.arguments
                            val l =
                                useClassInstance(owner, typeArgs, inReceiverContext).typeResult.id
                            if (isInterface || !owner.isInterfaceLike) {
                                tw.writeExtendsReftype(id, l)
                            } else {
                                tw.writeImplInterface(id.cast(), l.cast())
                            }
                        }
                        else -> {
                            logger.error(
                                "Unexpected simple type supertype: " +
                                    t.javaClass +
                                    ": " +
                                    t.render()
                            )
                        }
                    }
                }
                else -> {
                    logger.error("Unexpected supertype: " + t.javaClass + ": " + t.render())
                }
            }
        }
    }

    fun useValueDeclaration(d: IrValueDeclaration): Label<out DbVariable>? =
        when (d) {
            is IrValueParameter -> useValueParameter(d, null)
            is IrVariable -> useVariable(d)
            else -> {
                logger.error("Unrecognised IrValueDeclaration: " + d.javaClass)
                null
            }
        }

    /**
     * Returns `t` with generic types replaced by raw types, and type parameters replaced by their
     * first bound.
     *
     * Note that `Array<T>` is retained (with `T` itself erased) because these are expected to be
     * lowered to Java arrays, which are not generic.
     */
    fun erase(t: IrType): IrType {
        if (t is IrSimpleType) {
            val classifier = t.classifier
            val owner = classifier.owner
            if (owner is IrTypeParameter) {
                return eraseTypeParameter(owner)
            }

            if (owner is IrClass) {
                if (t.isBoxedArrayCodeQL) {
                    val elementType = t.getArrayElementTypeCodeQL(pluginContext.irBuiltIns)
                    val erasedElementType = erase(elementType)
                    return owner
                        .typeWith(erasedElementType)
                        .codeQlWithHasQuestionMark(t.isNullableCodeQL())
                }

                return if (t.arguments.isNotEmpty())
                    t.addAnnotations(listOf(RawTypeAnnotation.annotationConstructor))
                else t
            }
        }
        return t
    }

    private fun eraseTypeParameter(t: IrTypeParameter) = erase(t.superTypes[0])

    fun getValueParameterLabel(parentId: Label<out DbElement>?, idx: Int) =
        "@\"params;{$parentId};$idx\""

    /**
     * Gets the label for `vp` in the context of function instance `parent`, or in that of its
     * declaring function if `parent` is null.
     */
    fun getValueParameterLabel(vp: IrValueParameter, parent: Label<out DbCallable>?): String {
        val declarationParent = vp.parent
        val overriddenParentAttributes =
            (declarationParent as? IrFunction)?.let {
                (this as? KotlinFileExtractor)?.declarationStack?.findOverriddenAttributes(it)
            }
        val parentId = parent ?: overriddenParentAttributes?.id ?: useDeclarationParentOf(vp, false)

        val idxBase = overriddenParentAttributes?.valueParameters?.indexOf(vp) ?: parameterIndexExcludingReceivers(vp)
        val idxOffset =
            if (
                declarationParent is IrFunction &&
                    declarationParent.extensionReceiverParameter != null
            )
            // For extension functions increase the index to match what the java extractor sees:
            1
            else 0
        val idx = idxBase + idxOffset

        if (idx < 0) {
            // We're not extracting this and this@TYPE parameters of functions:
            logger.error("Unexpected negative index for parameter")
        }

        return getValueParameterLabel(parentId, idx)
    }

    fun useValueParameter(
        vp: IrValueParameter,
        parent: Label<out DbCallable>?
    ): Label<out DbParam> = tw.getLabelFor(getValueParameterLabel(vp, parent))

    private fun isDirectlyExposableCompanionObjectField(f: IrField) =
        f.hasAnnotation(FqName("kotlin.jvm.JvmField")) ||
            f.correspondingPropertySymbol?.owner?.let { it.isConst || it.isLateinit } ?: false

    private fun getFieldParent(f: IrField) =
        f.parentClassOrNull?.let {
            if (it.isCompanion && isDirectlyExposableCompanionObjectField(f)) it.parent else null
        } ?: f.parent

    fun isDirectlyExposedCompanionObjectField(f: IrField) = getFieldParent(f) != f.parent

    // Gets a field's corresponding property's extension receiver type, if any
    fun getExtensionReceiverType(f: IrField) =
        f.correspondingPropertySymbol?.owner?.let {
            (it.getter ?: it.setter)?.extensionReceiverParameter?.type
        }

    fun getFieldLabel(f: IrField): String {
        val parentId = useDeclarationParentOf(f, false)
        // Distinguish backing fields of properties based on their extension receiver type;
        // otherwise two extension properties declared in the same enclosing context will get
        // clashing trap labels. These are always private, so we can just make up a label without
        // worrying about their names as seen from Java.
        val extensionPropertyDiscriminator =
            getExtensionReceiverType(f)?.let { "extension;${useType(it).javaResult.id}" } ?: ""
        return "@\"field;{$parentId};$extensionPropertyDiscriminator${f.name.asString()}\""
    }

    fun useField(f: IrField): Label<out DbField> =
        tw.getLabelFor<DbField>(getFieldLabel(f)).also { extractFieldLaterIfExternalFileMember(f) }

    fun getPropertyLabel(p: IrProperty): String? {
        val parentId = useDeclarationParentOf(p, false)
        if (parentId == null) {
            return null
        } else {
            return getPropertyLabel(p, parentId, null)
        }
    }

    private fun getPropertyLabel(
        p: IrProperty,
        parentId: Label<out DbElement>,
        classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?
    ): String {
        val getter = p.getter
        val setter = p.setter

        val func = getter ?: setter
        val ext = func?.extensionReceiverParameter

        return if (ext == null) {
            "@\"property;{$parentId};${p.name.asString()}\""
        } else {
            val returnType =
                getter?.returnType
                    ?: setter?.valueParameters?.singleOrNull()?.type
                    ?: pluginContext.irBuiltIns.unitType
            val typeParams = getFunctionTypeParameters(func)

            getFunctionLabel(
                p.parent,
                parentId,
                p.name.asString(),
                listOf(),
                returnType,
                ext.type,
                typeParams,
                classTypeArgsIncludingOuterClasses,
                overridesCollectionsMethod = false,
                javaSignature = null,
                addParameterWildcardsByDefault = false,
                prefix = "property"
            )
        }
    }

    fun useProperty(
        p: IrProperty,
        parentId: Label<out DbElement>,
        classTypeArgsIncludingOuterClasses: List<IrTypeArgument>?
    ) =
        tw.getLabelFor<DbKt_property>(
            getPropertyLabel(p, parentId, classTypeArgsIncludingOuterClasses)
        ) {
            extractPropertyLaterIfExternalFileMember(p)
            extractPrivateSpecialisedDeclaration(p, classTypeArgsIncludingOuterClasses)
        }

    fun getEnumEntryLabel(ee: IrEnumEntry): String {
        val parentId = useDeclarationParentOf(ee, false)
        return "@\"field;{$parentId};${ee.name.asString()}\""
    }

    fun useEnumEntry(ee: IrEnumEntry): Label<out DbField> = tw.getLabelFor(getEnumEntryLabel(ee))

    fun getTypeAliasLabel(ta: IrTypeAlias): String {
        val parentId = useDeclarationParentOf(ta, true)
        return "@\"type_alias;{$parentId};${ta.name.asString()}\""
    }

    fun useTypeAlias(ta: IrTypeAlias): Label<out DbKt_type_alias> =
        tw.getLabelFor(getTypeAliasLabel(ta))

    fun useVariable(v: IrVariable): Label<out DbLocalvar> {
        return tw.getVariableLabelFor<DbLocalvar>(v)
    }
}
